import base64
import os
import urllib

import wx
import wx.lib.sized_controls as wxsc
from git import Repo, PushInfo
from robotide.action import ActionInfo
from robotide.pluginapi import Plugin, RideLogMessage
from robotide.widgets import PopupMenuItem


class GitPlugin(Plugin):
    """Git 插件，可以从代码仓库更新，上传用例"""

    OPERATE_DONE = 'DONE'
    OPERATE_FAILED = 'FAILED'
    OPERATE_CANCEL = 'CANCEL'

    def __init__(self, application):
        Plugin.__init__(self, application, name=None, doc=None, metadata=None,
                        default_settings=None, initially_enabled=True)
        self._app = application

        self._menu = [
            ActionInfo('Tools', '更新全部', self.git_update, position=91),
            ActionInfo('Tools', '全部提交', self.git_push_all, position=92),
            ActionInfo('Tools', '切换版本', self.git_switch_branch, position=93),
            ActionInfo('Tools', '创建新版本', self.git_create_branch, position=94)
        ]

        self._context_menu = []

    def _init_page_ignore_file(self, repo_path):
        ignore_file = os.path.join(repo_path, ".gitignore")
        if not os.path.exists(ignore_file):
            self.log(f"init git ignore file: {ignore_file}")
            with open(ignore_file, "w") as f:
                f.writelines(["__pycache__/\n", "*.pyc"])

    def git_create_branch(self, even):
        if not self._check_is_git_project():
            return

        dialog = InputDialog(title="GIT分支(当前版本：%s)" % self.case_active_branch, values={"版本名称": ""})
        if dialog.ShowModal() != wx.ID_OK:
            dialog.Destroy()
            return
        values = dialog.GetValue()
        dialog.Destroy()

        branch_name = values['版本名称']
        if branch_name is None or len(branch_name.strip()) == 0:
            wx.MessageBox("请输入版本名称！", "GIT错误", wx.OK | wx.ICON_ERROR)
            return

        try:
            if self._get_branch_head(self.case_path, branch_name):
                wx.MessageBox("本地已经存在这个版本 %s, 请使用切换版本！" % branch_name, "GIT错误", wx.OK | wx.ICON_ERROR)
                return
            self._git_create_branch(self.case_path, branch_name)
            self._git_create_branch(self.page_path, branch_name)
            wx.MessageBox("创建版本成功, 你当前已经在 %s 版本下" % branch_name, "GIT", wx.OK | wx.ICON_INFORMATION)
        except Exception as e:
            wx.MessageBox(str(e), "GIT错误", wx.OK | wx.ICON_ERROR)

    def git_switch_branch(self, even):
        if not self._check_is_git_project():
            return

        dialog = InputDialog(title="GIT分支(当前版本：%s)" % self.case_active_branch,
                             values={"版本名称": self._get_case_all_branches()})
        if dialog.ShowModal() != wx.ID_OK:
            dialog.Destroy()
            return
        values = dialog.GetValue()
        dialog.Destroy()
        branch_name = values['版本名称']

        try:
            head = self._get_branch_head(self.case_path, branch_name)
            if head is None:
                wx.MessageBox("本地没有这个版本 %s" % branch_name, "GIT错误", wx.OK | wx.ICON_ERROR)
                return
            head.checkout()

            head = self._get_branch_head(self.page_path, branch_name)
            if head is None:
                wx.MessageBox("Page版本切换失败，没有这个版本 %s" % branch_name, "GIT错误", wx.OK | wx.ICON_ERROR)
                return
            head.checkout()
            wx.MessageBox("切换版本成功, 你当前已经在 %s 版本下" % branch_name, "GIT", wx.OK | wx.ICON_INFORMATION)
            self._app._load_data()
        except Exception as e:
            wx.MessageBox(str(e), "GIT错误", wx.OK | wx.ICON_ERROR)

    def _get_branch_head(self, repo_path, branch):
        repo = Repo(repo_path)
        for head in repo.heads:
            if str(head) == branch:
                break
        else:
            return None
        return head

    def _git_create_branch(self, repo_path, branch_name):
        # https://github.com/gitpython-developers/GitPython/issues/471
        # https://www.devdungeon.com/content/working-git-repositories-python
        repo = Repo(repo_path)
        repo.git.branch(branch_name)
        repo.git.checkout(branch_name)
        repo.git.push("--set-upstream", 'origin', branch_name)

    def git_update(self, even):
        if not self._check_is_git_project():
            return

        repo_remote = None
        try:
            repo_remote = Repo(self.case_path)
            repo_remote.remote().pull()
            repo_remote = Repo(self.lib_path)
            repo_remote.remote().pull()
            repo_remote = Repo(self.page_path)
            repo_remote.remote().pull()

            dlg = wx.MessageDialog(None, "更新整个项目完成，点确定后重新加载项目", "GIT", wx.YES_NO | wx.ICON_QUESTION)
            if dlg.ShowModal() == wx.ID_YES:
                # 重新加载
                path = self._app._initial_path or self._app._get_latest_path()
                self.open_suite(path)
        except Exception as e:
            error_message = str(e)
            self.log(error_message)
            if 'stash them before you merge' in error_message:
                # 更新冲突，
                dlg = wx.MessageDialog(None, "更新文件冲突，是否尝试缓存本地修改之后再更新？\n\n详细信息: %s" % error_message, "GIT", wx.YES_NO | wx.ICON_ERROR)
                if dlg.ShowModal() == wx.ID_YES:
                    repo_remote.git.stash()
                    wx.MessageBox("你本地的修改已缓存，点击【OK】后，用例会重新加载，请加载完后再次更新!", "提示", wx.OK | wx.ICON_INFORMATION)
                    path = self._app._initial_path or self._app._get_latest_path()
                    self.open_suite(path)
            else:
                wx.MessageBox(str(e), "GIT错误", wx.OK | wx.ICON_ERROR)

    def git_push(self, even):
        if not self._check_is_git_project():
            return

        repo_path = self._get_selected_git_path()
        if repo_path is None:
            wx.MessageBox("你没有选择文件或是选择的目录不能提交! %s" % self._get_selected_path(), "GIT错误", wx.OK | wx.ICON_ERROR)
            return

        add_file = self._get_selected_path()
        dlg = wx.MessageDialog(None, "确认提交此文件修改？\n%s" % add_file, "GIT", wx.YES_NO | wx.ICON_QUESTION)
        if dlg.ShowModal() == wx.ID_NO:
            return

        result, message = self._git_push(repo_path, add_file)
        if result == GitPlugin.OPERATE_DONE:
            wx.MessageBox("Git提交成功", "GIT", wx.OK | wx.ICON_INFORMATION)
        else:
            if 'non-fast-forward' in message or 'fetch first' in message:
                wx.MessageBox(f"服务端文件已更改，请先更新后再提交!\n\n文件：{add_file}\n详情：{message}", "GIT错误", wx.OK | wx.ICON_ERROR)
            else:
                wx.MessageBox("Git提交失败\n" + message, "GIT", wx.OK | wx.ICON_ERROR)

    def git_push_all(self, even):
        if not self._check_is_git_project():
            return

        dlg = wx.MessageDialog(None, "确认提交所有用例和Page修改？", "GIT", wx.YES_NO | wx.ICON_QUESTION)
        if dlg.ShowModal() == wx.ID_NO:
            return

        result_case, message_case = self._git_push(self.case_path, self.case_path)
        result_page, message_page = self._git_push(self.page_path, self.page_path)
        if result_case == GitPlugin.OPERATE_FAILED:
            if 'non-fast-forward' in message_case or 'fetch first' in message_case:
                show_message = f"服务端文件已更改，请先更新后再提交！\n\n详情：{message_case}"
            else:
                show_message = f"Git提交失败！\n\n错误详情：{message_case}"
            wx.MessageBox(show_message, "提交用例失败", wx.OK | wx.ICON_ERROR)
        elif result_page == GitPlugin.OPERATE_FAILED:
            if 'non-fast-forward' in message_page or 'fetch first' in message_page:
                show_message = f"服务端文件已更改，请先更新后再提交！\n\n详情：{message_page}"
            else:
                show_message = f"Git提交失败！\n\n错误详情：{message_page}"
            wx.MessageBox(show_message, "提交Page层失败", wx.OK | wx.ICON_ERROR)
        else:
            wx.MessageBox("Git提交成功", "GIT", wx.OK | wx.ICON_INFORMATION)

    def _try_push(self, old_url, username, password, origin, add_file, repo):
        new_url = None
        try:
            new_url = self._get_author_url(old_url, username, password)
            if new_url is None:
                return False, "远程地址非法\n %s" % old_url
            # self.log(new_url)
            # self.log(old_url)
            origin.set_url(new_url, old_url)
            # 提交
            self.log(f'git add {add_file}')
            repo.git.add(add_file)
            repo.index.commit('Ride Plugin Commit')
            # PUSH
            results = origin.push()
            return True, results
        except Exception as e:
            return False, str(e)
        finally:
            if new_url is not None and old_url is not None:
                # 需要把鉴权的url地址换回来
                origin.set_url(old_url, new_url)



    def _git_push(self, repo_path, add_file):

        if not os.path.exists(repo_path):
            self.log(f"PUSH ERROR, {repo_path} not exist!")
            return


        # 尝试初始化忽略文件
        # if repo_path == self.page_path:  # Case目录下也有可能Lib类的关键字
        self._init_page_ignore_file(repo_path)

        username, password = self._get_git_account_password()
        if username is None or password is None:
            return

        new_url, old_url = None, None
        try:
            # 判断是否有修改
            repo = Repo(repo_path)
            if not repo.is_dirty(untracked_files=True):
                return GitPlugin.OPERATE_CANCEL, "文件没有变动，不需要提交！"
            # 增加鉴权信息
            origin = repo.remote()
            old_url = next(origin.urls)
            # new_url = self._get_author_url(old_url, username, password)
            # if new_url is None:
            #     return GitPlugin.OPERATE_FAILED, "远程地址非法\n %s" % old_url
            # # self.log(new_url)
            # # self.log(old_url)
            # origin.set_url(new_url, old_url)
            # # 提交
            # self.log(f'git add {add_file}')
            # repo.git.add(add_file)
            # repo.index.commit('Ride Plugin Commit')
            # # PUSH
            # error_message = ""
            # results = origin.push()
            num = 3
            while num:
                flag, results = self._try_push(old_url, username, password, origin, add_file, repo)
                if flag:
                    break
                if "Authentication failed for" in results:
                    user_name, password = self._get_account_password_from_box()
                    num -= 1
                else:
                    if repo:
                        # 提交回退
                        repo.index.reset(commit='HEAD~1', head=True)
                    return GitPlugin.OPERATE_FAILED, results
                if not num:
                    return GitPlugin.OPERATE_FAILED, results

            # 如果成功，保存帐号信息
            self.save_setting('git_user_name', username)
            self.save_setting('git_password', base64.b64encode(password.encode('utf-8')).decode())

            error_message = ""
            if results:
                # 判断提交是否成功
                for result in results:
                    if result.flags & PushInfo.ERROR:
                        error_message += '\n' + result.summary

            if error_message:
                # 提交回退
                repo.index.reset(commit='HEAD~1', head=True)
                return GitPlugin.OPERATE_FAILED, error_message
            else:
                return GitPlugin.OPERATE_DONE, error_message
        except Exception as e:
            if repo:
                # 提交回退
                repo.index.reset(commit='HEAD~1', head=True)
            return GitPlugin.OPERATE_FAILED, str(e)
        finally:
            if new_url is not None and old_url is not None:
                # 需要把鉴权的url地址换回来
                origin.set_url(old_url, new_url)

    def get_context_menu(self, data):
        repo_path = self._get_selected_git_path()
        if repo_path is None:
            self._context_menu = []
        else:
            branch_name = self._get_active_branch(repo_path)
            self._context_menu = [
                PopupMenuItem("---"),
                PopupMenuItem("全部更新[%s]" % branch_name, self.git_update, self),
                PopupMenuItem("提交此文件[%s]" % branch_name, self.git_push, self)
            ]
        return self._context_menu

    def enable(self):
        self.unregister_actions()
        self.register_actions(self._menu)
        self.tree.register_context_menu_hook(self.get_context_menu)

    def disable(self):
        self.unregister_actions()
        self.tree.unregister_context_menu_hook(self.get_context_menu)

    @property
    def case_path(self):
        return os.path.join(self._root_path(), 'Case')

    @property
    def lib_path(self):
        return os.path.join(self._root_path(), 'Lib')

    @property
    def page_path(self):
        return os.path.join(self._root_path(), 'Page')

    @property
    def case_active_branch(self):
        return self._get_active_branch(self.case_path)

    @property
    def page_active_branch(self):
        return self._get_active_branch(self.page_path)

    def _get_author_url(self, url, username, password):
        en_userName = urllib.parse.quote(username)
        en_password = urllib.parse.quote(password)
        if url.startswith('http://'):
            new_url = 'http://' + en_userName + ':' + en_password + '@' + url[7:]
        elif url.startswith('https://'):
            new_url = 'https://' + en_userName + ':' + en_password + '@' + url[8:]
        else:
            return None
        return new_url

    def _get_account_password_from_box(self):
        user_name, password = None, None
        dialog = InputDialog(title="GIT帐号", values={"用户名": "", "密码": ""})
        if dialog.ShowModal() == wx.ID_OK:
            values = dialog.GetValue()
            user_name = values['用户名']
            password = values['密码']
        dialog.Destroy()
        return user_name, password

    def _get_git_account_password(self):
        user_name, password = None, None
        try:
            user_name = self.git_user_name
            password = base64.b64decode(self.git_password).decode()
        except AttributeError as e:
            # dialog = InputDialog(title="GIT帐号", values={"用户名": "", "密码": ""})
            # if dialog.ShowModal() == wx.ID_OK:
            #     values = dialog.GetValue()
            #     user_name = values['用户名']
            #     password = values['密码']
            # dialog.Destroy()
            user_name, password = self._get_account_password_from_box()
        return user_name, password

    def _check_is_git_project(self):
        if self.case_active_branch is None:
            wx.MessageBox("你打开的项目不是GIT版本控制下！", "GIT错误", wx.OK | wx.ICON_ERROR)
            return False
        return True

    def _get_selected_path(self):
        file = self.get_selected_datafile()
        if file is not None:
            path = os.path.join(file.directory, file.source)
            return path
        return None

    def _get_selected_git_path(self):
        selected_file = self._get_selected_path()
        if self.case_path in selected_file:
            return self.case_path
        elif self.page_path in selected_file:
            return self.page_path
        else:
            return None

    def _get_case_all_branches(self):
        try:
            repo = Repo(self.case_path)
            result = []
            for branch in repo.branches:
                result.append(str(branch))
            return result
        except Exception as e:
            return None

    def _get_active_branch(self, repo_path):
        try:
            repo = Repo(repo_path)
            return repo.active_branch
        except Exception as e:
            return None

    def _root_path(self):
        return self._app._initial_path or self._app._get_latest_path()

    def log(self, message):
        mess = RideLogMessage(message=message)
        mess.publish()


class InputDialog(wxsc.SizedDialog):
    # ~ #set value for widgets( StaticText and TextCtrl) height
    wh = 30
    # ~ #set value for max width times
    mwt = 4
    # ~ #set value for  wh times
    wht = 3

    def __init__(self, title, values):
        '''
        #~ using it as follow:
        #~ dialog = InputDialog(title='Setting values:',values={'int':1,'String':'This is String','float':3.5})
        #~ just for test:
        #~ dialog = InputDialog()
        '''
        style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
        wxsc.SizedDialog.__init__(self, parent=None, id=-1, title=title, style=style)

        self.originvalues = values.copy()
        self.modifiedvalues = values.copy()
        self.pane = self.GetContentsPane()
        self.pane.SetSizerType("form")

        maxlen1 = 60
        maxlen2 = 200

        for key in self.modifiedvalues:
            keyStr = str(key)
            label = keyStr + ' :'
            StaticText = wx.StaticText(parent=self.pane, id=-1, label=label, style=wx.ALIGN_LEFT)
            StaticText.SetInitialSize((maxlen1, self.wh))

            value = self.modifiedvalues[key]
            if isinstance(value, list):
                TextCtrl = wx.ComboBox(parent=self.pane, id=-1, value='master', choices=value, style=wx.CB_READONLY)
            else:
                TextCtrl = wx.TextCtrl(parent=self.pane, id=-1, value=value)
            TextCtrl.SetInitialSize((maxlen2, self.wh))
            TextCtrl.SetSizerProps(expand=True)
            # ~set a name for TextCtrl,so later we can use wx.FindWindowByName()
            TextCtrl.Name = 'TC_' + str(keyStr)
            # StaticText.Name='ST_'+str(keyStr)

        # ~ # add dialog buttons
        self.SetButtonSizer(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL))

        self.Fit()
        self.Center()

    def GetOriginValue(self):
        '''
        #~ if the user select wx.ID_CANCEL,then return originvalues
        '''
        return self.originvalues

    def GetValue(self):
        '''
        #~ if the user select wx.ID_OK,then return self.modifiedvalues
        '''
        for key in self.modifiedvalues:
            keyStr = str(key)
            TextCtrlName = 'TC_' + str(keyStr)
            TextCtrl = self.FindWindowByName(TextCtrlName)
            ovk = self.modifiedvalues[key]
            self.modifiedvalues[key] = str(TextCtrl.GetValue())

        return self.modifiedvalues
